import bpy
import random
from uuid import uuid4
from typing import Union, List
from bpy.props import BoolProperty
from os.path import join, splitext
from bpy.types import Context, Material, Object, Scene, Collection
from ...addon.naming import FluidLabNaming
from ...addon.paths import FluidLabPreferences
from ...libs.functions.basics import ocultar_post_panel_settings, context_override
from ...libs.functions.object import name_with_zfill, add_single_vertext_ob
from ...libs.functions.modifiers import create_modifier
from ...libs.functions.common_ui_elements import multiline_print
from ...libs.functions.geometry_nodes import set_exposed_attributes_of_gn, add_node, connect_nodes, append_gn
from ...libs.functions.get_common_vars import get_common_vars
from ...libs.functions.materials import append_material
from ...libs.functions.collections import create_new_collection, set_active_collection, set_active_collection_to_master_coll, set_active_collection_by_name
from .op_list_reuse_base import FLUIDLAB_OT_list_reuse_base
from datetime import datetime


class FLUIDLAB_OT_add_gn_mesh(FLUIDLAB_OT_list_reuse_base):
    bl_idname = "fluidlab.add_gn_mesh"
    bl_label = "Add GN Meshes To:"
    bl_description = "Add GN Meshes"
    bl_options = {"REGISTER", "UNDO"}
    
    
    def assing_material(self, context:Context, ob:Object, material:Material, slot:int=0) -> None:
        
        # Si recibe los inputs adecuados:
        if isinstance(material, Material) and isinstance(ob, Object):
            
            # Si el slot deseado es mayor a los slots exitentes creaos un slot nuevo:
            if slot >= len(ob.material_slots):
                ob.data.materials.append(None)

            # Asignamos el material en el primer slot:
            ob.data.materials[slot] = material
    

    def tint_node_red(self, node) -> None:
        node.use_custom_color = True
        node.color = (0.608, 0.0100094, 0)


    def config_gn(self, context, single_vert_ob, alives_vertexs_obs, node_group, material1, material2) -> List[str]:

        gn_mod = create_modifier(single_vert_ob, FluidLabNaming.GN_MESH_MOD, 'NODES')
        if gn_mod:
            
            fifty = 50
            y_distance = fifty*4       # 200
            direction = 0 - y_distance # -200

            nodes = node_group.nodes
            join_geo = nodes.get("JG")
            if join_geo:

                # Obtenemos el nodo Index Switch:
                index_switch_node_name = FluidLabNaming.INDEX_SW_COLORS
                index_switch_node = nodes.get(index_switch_node_name)
                
                all_stored_ob_id = []

                # Creando por código los Object Info:
                for i, emitter_ob in enumerate(alives_vertexs_obs):
                    n_loc_y = i * (direction)

                    ob_info = add_node(node_group, (join_geo.location.x-400, n_loc_y), "GeometryNodeObjectInfo", False)
                    ob_info.inputs[0].default_value = emitter_ob
                    ob_info.transform_space = 'RELATIVE'
                    
                    store_ob_id = add_node(node_group, (join_geo.location.x-200, n_loc_y), "GeometryNodeStoreNamedAttribute", False)
                    # Seteamos que es de tipo integer:
                    store_ob_id.data_type = 'INT'
                    # Seteamos el Attribute Name:
                    store_ob_id.inputs[2].default_value = "fl_id"
                    # Le seteamos el ID:
                    store_ob_id.inputs[3].default_value = i
                    all_stored_ob_id.append(store_ob_id)

                    # conectamos:
                    connect_nodes(node_group, ob_info, "Geometry", store_ob_id, "Geometry", debug=False)

                    if index_switch_node:
                        # Agrego un nuevo item en modo low level:
                        index_switch_node.index_switch_items.new()
                        color_slots = index_switch_node.inputs[0:-1] # <- para evitar pillar el inputs["Index"]
                        # Seteamos el color random al ultimo (osea al recien creado):
                        color_slots[-1].default_value = (random.random(), random.random(), random.random(), 1)
                
                # Conectamos el join en su orden correcto:
                for store_ob_id in reversed(all_stored_ob_id):
                    connect_nodes(node_group, store_ob_id, "Geometry", join_geo, "Geometry", debug=False)


            # Para ir finalizando:
            # Lo seteamos al GN mod:
            gn_mod.node_group = node_group
            single_vert_ob.modifiers.active = gn_mod

            #--------------------------------------------------------------------------------------------------------------------------- 
            # Seteando valores expuestos externamente en el GN:<
            #--------------------------------------------------------------------------------------------------------------------------- 
            # set_exposed_attributes_of_gn(gn_mod, "Voxel Resolution", 0.05, debug=False)
            # set_exposed_attributes_of_gn(gn_mod, "Min", 0.0, debug=False)
            # set_exposed_attributes_of_gn(gn_mod, "Max", 0.16, debug=False)

            set_exposed_attributes_of_gn(gn_mod, "Material", material1, debug=False)
            set_exposed_attributes_of_gn(gn_mod, "Grain Material", material2, debug=False)
            #---------------------------------------------------------------------------------------------------------------------------
        
    

    def prepare_name(self, ext:str) -> Union[str, None]:
        final_name = None
        now = datetime.now()
        date_hash = "_moth_" + str(now.month) + "_day_" + str(now.day) + "_" + str(now.hour) + "h_" + str(now.minute) + "m_" + str(now.second) + "s" + "_dots" + ext 
        
        if bpy.data.is_saved:
            blend_file_name = bpy.path.basename(bpy.context.blend_data.filepath)
            name = splitext(blend_file_name)[0] 
            final_name = name + date_hash
        else:
            final_name = "unsaved_scene" + date_hash
        
        return final_name
    

    def alembic_export(self, fpath:str, scn:Scene) -> None:
        # chuleta https://docs.blender.org/api/current/bpy.ops.wm.html#bpy.ops.wm.alembic_export
        bpy.ops.wm.alembic_export(
            filepath=fpath, 
            check_existing=True, 
            start=scn.frame_start, 
            end=scn.frame_end, 
            xsamples=1, 
            gsamples=1,  
            selected=True, 
            visible_objects_only=True, 
            flatten=False,  # Aplanar la jerarquía (opcional)
            uvs=True, 
            packuv=True, 
            normals=True, 
            vcolors=False, 
            orcos=True,  # Exportar información de color (opcional)
            face_sets=False, 
            subdiv_schema=False, 
            apply_subdiv=True, 
            curves_as_mesh=True, 
            use_instancing=True, 
            global_scale=1.0, 
            triangulate=False,
            quad_method='SHORTEST_DIAGONAL', 
            ngon_method='BEAUTY', 
            export_custom_properties=True, 
            as_background_job=False,  # Ejecutar como trabajo en segundo plano (opcional) (Para ser sincrono tiene q ser False)
            evaluation_mode='VIEWPORT', 
            init_scene_frame_range=True 
        )
    

    def alembic_import(self, fpath:str) -> None:
        # chuleta import https://docs.blender.org/api/current/bpy.ops.wm.html#bpy.ops.wm.alembic_import
        bpy.ops.wm.alembic_import(
            filepath=fpath, 
            relative_path=True, 
            filter_glob='*.abc', 
            scale=1.0, 
            set_frame_range=True, 
            validate_meshes=False, 
            always_add_cache_reader=True, 
            is_sequence=False, 
            as_background_job=False
        )


    def prepare_main_collections(self, context, valid_groups) -> Collection:

        # Colección Meshes:
        meshes_coll = bpy.data.collections.get(FluidLabNaming.MESHES_COLL)
        if not meshes_coll:
            meshes_coll = create_new_collection(context, FluidLabNaming.MESHES_COLL)
        set_active_collection(context, meshes_coll)
        
        place = bpy.data.collections
        first_valid_group = valid_groups[0]
        desired_name = FluidLabNaming.MESH_PREFIX + first_valid_group.label_txt.split("_")[1] if "_" in first_valid_group.label_txt else first_valid_group.label_txt
        intermediate_coll_name = name_with_zfill(context, desired_name, place)
        
        # Intermediate Group Collection (para poner dentro sus correspondientes GN "emitters")
        intermediate_coll = bpy.data.collections.get(intermediate_coll_name)
        if not intermediate_coll:
            intermediate_coll = create_new_collection(context, intermediate_coll_name, False)

        set_active_collection(context, intermediate_coll)

        return intermediate_coll
    

    def prepare_abc_imported_collections(self, context) -> None:

        # Si no existe se crea y se deja activa FluidLab:
        set_active_collection_to_master_coll(context)

        target_coll_name = FluidLabNaming.EXPORTS_ABC

        # Group collection:
        new_coll = bpy.data.collections.get(target_coll_name)
        if not new_coll:
            new_coll = create_new_collection(context, target_coll_name, False)

        set_active_collection_by_name(context, target_coll_name)
    

    # Para usar alembic:
    use_alembic: BoolProperty(default=False)
    show_unborn: BoolProperty(default=False)


    def draw(self, context):

        layout = self.layout
        layout.use_property_decorate = False
        layout.use_property_split = False

        fluid_groups = get_common_vars(context, get_fluid_groups=True)
        active_group = fluid_groups.active

        if active_group:
        
            listados = layout.row(align=True)
            listados.template_list("FLUIDLAB_UL_draw_fluids_groups", "", fluid_groups, "list", fluid_groups, "list_index", rows=3)
        
            fluid_emitters = active_group.emitters
            listados.template_list("FLUIDLAB_UL_draw_fluids_emitters", "", fluid_emitters, "list", fluid_emitters, "list_index", rows=3)

        #-----------------------------------------------------------
        # Opciones de UI para usar el workflow de alembic:
        #-----------------------------------------------------------

        layout.prop(self, "use_alembic", text="Use Alembic WorkFlow")
        if self.use_alembic:

            red_lay = layout.row(align=True)
            red_lay.alert = True
            text="Your particulate emitters will be deleted"
            multiline_print(red_lay, text, 6, 'ERROR')

            text="When creating the mesh with Alembic, particles are exported, real particles are deleted, and the mesh is generated with this Alembic."
            multiline_print(layout, text, 6, 'INFO')

            layout.prop(self, "show_unborn", text="Use Unborn")

            addon_preferences = FluidLabPreferences.get_prefs(context)
            fpath = addon_preferences.exports_path
            if not fpath:
                path_prop = layout.row(align=True)
                path_prop.alert = True
                path_prop.prop(addon_preferences, "exports_path", text="Path")
        
        #-----------------------------------------------------------


    def execute(self, context):
        start = datetime.now()

        scn, fluid_groups, fluid_mesh = get_common_vars(context, get_scn=True, get_fluid_groups=True, get_fluid_mesh=True)

        if self.use_alembic:
            addon_preferences = FluidLabPreferences.get_prefs(context)
            fpath = addon_preferences.exports_path
            if not fpath:
                self.report({'ERROR'}, "Not valid exports path!")
                return {'CANCELLED'}
        
        all_groups = fluid_groups.get_all_items
        if not all_groups:
            self.report({'ERROR'}, "No groups were detected!")
            return {'CANCELLED'}
       
        valid_groups = [group for group in all_groups if group.meshme]

        if not valid_groups:
            self.report({'ERROR'}, "No groups marked were detected!")
            return {'CANCELLED'}

        intermediate_coll = self.prepare_main_collections(context, valid_groups)
        if not intermediate_coll:
            print("No intermediate collection!")
            self.report({'ERROR'}, "No intermediate collection detected!")
            return {'CANCELLED'}
        
        single_vert_ob = add_single_vertext_ob(ob_name=intermediate_coll.name, mesh_name=intermediate_coll.name, collection=intermediate_coll)
        if not single_vert_ob:
            print("No single vertex!")
            self.report({'ERROR'}, "No single vertex detected!")
            return {'CANCELLED'}

        # Capturamos los objetos y los items (los items de emitters, por si se usa alembic saber luego cuales borrar al terminar)
        all_data = []

        # all_data = [ {"group": group, "item": emitter_item, "ob": emitter_ob}, {"group": group, "item": emitter_item, "ob": emitter_ob}]
        for group in valid_groups:
            
            emitters_list = group.emitters
            all_emitters_items = emitters_list.get_all_items
            data = {"group": group}                                                             # <- group_items_mesheables
            
            for emitter_item in all_emitters_items:
                if emitter_item.meshme:
                    emitter_ob = next((ob for ob in emitter_item.group_coll.objects if ob.fluidlab.id_name == "Emitter_"+ emitter_item.id_name), None)
                    data.setdefault("items", []).append(emitter_item)                           # <- valid_emitter_items
                    data.setdefault("objs", []).append(emitter_ob)                              # <- emitters_obs
                    data.setdefault("alive_vertex_objs", []).append(emitter_item.vertex_ob)     # <- alives_vertexs_obs
                    data.setdefault("dead_vertex_objs", []).append(emitter_item.vertex_ob_dead) # <- dead_vertexs_obs
                    
            all_data.append(data)
        
        # valid_emitter_items = [it for ad in all_data if "items" in ad for it in ad["items"] ]
        emitters_obs = [ob for ad in all_data if "objs" in ad  for ob in ad["objs"]]
        alives_vertexs_obs = [vob for ad in all_data  if "alive_vertex_objs" in ad for vob in ad["alive_vertex_objs"] if FluidLabNaming.ALIVE in vob]
        dead_vertexs_obs = [vob for ad in all_data  if "dead_vertex_objs" in ad for vob in ad["dead_vertex_objs"] if FluidLabNaming.DEAD in vob]

        total_v_ob = len(alives_vertexs_obs)
        if total_v_ob <= 0:
            self.report({'ERROR'}, "No emitters marked were detected!")
            return {'CANCELLED'}

        #-----------------------------------------------
        # Si estoy usando alembic exporto en alembic:
        #-----------------------------------------------
        if self.use_alembic:

            # Activamos el unborn si el usuario quere usarlo:
            if self.show_unborn:
                
                # a los emisores de particulas les activamos el unborn en True
                for ob in emitters_obs:
                    for psys in ob.particle_systems: 
                        if psys.settings.fluidlab.id_name == "":
                            continue
                        psys.settings.show_unborn = self.show_unborn

                # a los modifiers de los alive les activamos el unborn a true
                for alive_ob in alives_vertexs_obs:
                    p_instance_mod = alive_ob.modifiers.get(FluidLabNaming.PARTICLE_INSTANCE_ALIVE_MOD)
                    if p_instance_mod:
                        p_instance_mod.show_unborn = self.show_unborn
            
            bpy.ops.object.select_all(action='DESELECT')
            [(ob.hide_set(False), ob.select_set(True)) for ob in alives_vertexs_obs] # des-oculto con ojito y despues selecciono
            [(ob.hide_set(False), ob.select_set(True)) for ob in dead_vertexs_obs]   # des-oculto con ojito y despues selecciono

            # Desactivamos los modificadores donde se computan los attributos para que pueda exportarse bien el alembic:
            for alive_ob in alives_vertexs_obs:
                for mod in alive_ob.modifiers:
                    if mod.type != 'NODES':
                        continue
                    if not mod.name.startswith(FluidLabNaming.GN_MESH_MOD):
                        continue
                    mod.show_viewport = False
                    mod.show_render = False

            # Preparando el path name:
            final_name = self.prepare_name(".abc")

            # Preparando el path del archivo.ext:
            fpath = join(fpath, final_name)
            self.alembic_export(fpath, scn)

            # los vuelvo a ocultar:
            [ob.hide_set(True) for ob in alives_vertexs_obs]  # ocultamos los alive con el ojito
            [ob.hide_set(True) for ob in dead_vertexs_obs]    # ocultamos los dead con el ojito
    
            # los objetos exitentes previamente:
            previous_obs = bpy.data.objects[:]

            # importamos el alembic:
            self.prepare_abc_imported_collections(context)
            self.alembic_import(fpath)

            # estipulamos los nuevos emitter ob (los alembic importados):
            alives_vertexs_obs = [ob for ob in context.view_layer.objects if ob not in previous_obs and "alive" in ob.name.lower()]
            [ob.hide_set(True) for ob in alives_vertexs_obs] # oculto con ojito (los nuevos objetos vertex importados)

            dead_vertexs_obs = [ob for ob in context.view_layer.objects if ob not in previous_obs and "dead" in ob.name.lower()]
            [ob.hide_set(True) for ob in dead_vertexs_obs] # oculto con ojito (los nuevos objetos vertex importados)

            # Les ponemos el modifier a los alive apuntando a sus deads:
            alive_node_group = append_gn(context=context, file_name="Alive_GN", gn_name=FluidLabNaming.GN_PART_ATTRS, only_if_not_exist=True, is_unique=True)

            # Relaciono los pares (Alive, dead) por el id del nombre:
            #--------------------------------------------------------
            def extract_id(obj):
                return obj.name.split('_')[1]

            # Crear diccionarios para almacenar los objetos por ID
            alive_dict = {extract_id(obj): obj for obj in alives_vertexs_obs}
            dead_dict = {extract_id(obj): obj for obj in dead_vertexs_obs}

            # Crear la lista de pares (Alive, Dead)
            paired_objects = [(alive_dict[id], dead_dict[id]) for id in alive_dict if id in dead_dict]

            # Función para configurar los modificadores
            def configure_modifiers(alive_ob, dead_ob, alive_node_group):
                part_inst_alive_mod = create_modifier(alive_ob, FluidLabNaming.GN_PART_ATTRS_MOD, 'NODES')
                if part_inst_alive_mod:
                    part_inst_alive_mod.node_group = alive_node_group
                    set_exposed_attributes_of_gn(part_inst_alive_mod, "Object", dead_ob, debug=False)
                    alive_ob.modifiers.active = part_inst_alive_mod

            # Procesar cada par de objetos
            for alive_ob, dead_ob in paired_objects:
                configure_modifiers(alive_ob, dead_ob, alive_node_group)
            #--------------------------------------------------------
            

        #-----------------------------------------------

        # APPEND:
        node_group = append_gn(context=context, file_name="Mesh_GN", gn_name=FluidLabNaming.GN_MESH, only_if_not_exist=True, is_unique=True)

        blend_file = "FLab_BasicShaders"
        
        material_name1 = "FLuidLab_Basic_Shader"
        # material_name1 = "FL_Water"

        material_name2 = "FLuidLab_Basic_Shader_Grain"
        # material_name2 = "FL_Sand_Grain"

        material1 = append_material(context, blend_file, material_name1)
        material2 = append_material(context, blend_file, material_name2)
        
        self.assing_material(context, single_vert_ob, material1, 0)
        self.assing_material(context, single_vert_ob, material2, 1)

        # Configuramos el GN por dentro con código
        self.config_gn(context, single_vert_ob, alives_vertexs_obs, node_group, material1, material2)

        id_name = str(uuid4())[:6]
        
        # Guardo las colecciones de los grupos originales de donde eligio crear el mesh el usuario:
        valid_collections_groups = [group.group_coll for group in valid_groups]

        fluid_mesh.add_item(id_name, intermediate_coll.name, intermediate_coll, single_vert_ob, valid_collections_groups)

        # Restauro los index (Fluid Group y Emitters list) previos que tuviera el usuario:
        self.restore_indexs(context)
        ocultar_post_panel_settings()

        # Si usamos alembic, se borran los emisores de particulas:
        if self.use_alembic:
            
            for data in reversed(all_data):
                
                # Primero limpio sus caches:
                if "objs" in data:
                    for emitter_ob in reversed(data["objs"]): 
                        for psys in emitter_ob.particle_systems:        
                            # Solo trabajamos con nuestras particulas:
                            if psys.settings.fluidlab.id_name == "":
                                continue
                            with context.temp_override(object=emitter_ob, point_cache=psys.point_cache):
                                bpy.ops.ptcache.free_bake()
                
                # Despues eliminamos el item:
                if "items" in data:
                    for emitter_item in reversed(data["items"]):
                        # print(emitter_item.label_txt)
                        # remove_item(emitter_item.id_name)
                        emitter_item.remove = True
                
                # se borran los grupos vacios de emisores:
                for group in reversed(valid_groups):
                    if group.emitters.length == 0:
                        group.remove = True

        set_active_collection_to_master_coll(context)

        print("[fluidlab.add_gn_mesh] Time: " + str(datetime.now() - start))
        return {'FINISHED'}